Skip to content

feat(byok): add @tanstack/ai-byok bring-your-own-key toolkit#906

Open
tombeckenham wants to merge 12 commits into
mainfrom
byok-package
Open

feat(byok): add @tanstack/ai-byok bring-your-own-key toolkit#906
tombeckenham wants to merge 12 commits into
mainfrom
byok-package

Conversation

@tombeckenham

@tombeckenham tombeckenham commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds @tanstack/ai-byok — a bring-your-own-key toolkit for TanStack AI. Users supply their own provider keys; the library collects them client-side, attaches them per-request in a header, and uses them server-side without ever persisting or logging them.

First principle: never be a custodian. Keys live client-side (the browser is the system of record). The server piece is a stateless pass-through — it reads the key off the incoming request header, hands it to the adapter for one call, and never writes it to a DB, cache, log, or observability stream. No central endpoint is baked in, so third parties self-host their own relay.

Keys travel in x-tanstack-byok-<provider>never the request body or message history — so they stay out of persisted conversations and the event/observability stream.

Package layout

Entry Contents
@tanstack/ai-byok Provider registry + ProviderId, byokHeaders, storage (memoryStorage default / passkeyStorage), validateKey, isPasskeyStorageSupported
@tanstack/ai-byok/server getByokKey (header-only, never logged), byokMissing (typed 401), scrubSecrets/maskKey
@tanstack/ai-byok/react <ByokProvider storage={…}>, useByok() (with locked/unlock), drop-in <ByokKeyManager> (last-4 display only)

Storage — no plaintext

  • memoryStorage() (default) — session-only, nothing persisted, zero at-rest liability.
  • passkeyStorage() (opt-in) — encrypted at rest: keyring in IndexedDB as AES-256-GCM ciphertext, key derived from a passkey's WebAuthn PRF output via HKDF, unwrapped on demand with a biometric/PIN tap. Fully client-side — no server, no custodian. Feature-detect with isPasskeyStorageSupported(), fall back to memory. Protects at-rest theft, not live in-page XSS (documented). Binds to the current origin by default (rpId configurable). No plaintext persistence option by design.

Example + E2E

  • Example (examples/ts-react-chat): /byok route (ByokProvider + ByokKeyManager + chat sending byokHeaders) and /api/byok-chat relay (getByokKey → per-request adapter → byokMissing).
  • E2E (testing/e2e/tests/byok.spec.ts): drives a real browser through the relay — asserts the key rides in the header and is absent from the request body, streams the aimock response, shows only last-4; and that a missing key surfaces byokMissing with no answer. Keyring is hydrated via a preloaded storage (the same load() path passkey uses), so it's deterministic without a live WebAuthn ceremony.

Tests

Unit: byokHeaders, getByokKey, byokMissing, scrub, passkey AES-GCM round-trip + tamper, useByok set/clear + locked/unlock. Package gate green: test:types, test:eslint, test:lib (10), test:build/publint, build, test:sherif, test:knip, test:docs. E2E BYOK specs green.

Follow-ups

  • A docs/ page.
  • Optional: a metadata-only BYOK devtools panel (provider presence / last-4 / validation status / storage tier — never the key).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added @tanstack/ai-byok for end-to-end BYOK with React key management (session memory or optional passkey-encrypted storage), plus stateless server helpers.
    • BYOK keys are attached per request via provider-specific headers only, with UI masking and unlock support.
    • Updated the TypeScript React chat example and added new end-to-end BYOK routes/tests.
  • Bug Fixes
    • Improved missing/locked-key handling and unlock prompting using typed missing-key detection.
  • Tests
    • Added unit tests for header behavior, key validation, storage/persistence, and passkey crypto, plus Playwright e2e coverage.

Client keyring, per-provider request headers, and stateless server
helpers that never persist or log provider keys. Keys live client-side
and travel in an x-tanstack-byok-<provider> header, never the request
body or message history.

- @tanstack/ai-byok: provider registry, byokHeaders, pluggable storage
  (memory default, opt-in plaintext localStorage), validateKey
- /react: <ByokProvider storage>, useByok, drop-in <ByokKeyManager>
  (last-4 display only)
- /server: getByokKey (header-only, never logged), byokMissing (typed
  error), scrubSecrets/maskKey

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 68c6476f-4d96-486b-be5f-15002ca9be6f

📥 Commits

Reviewing files that changed from the base of the PR and between e0233be and 31a8d72.

📒 Files selected for processing (6)
  • .changeset/byok-package.md
  • packages/ai-byok/README.md
  • packages/ai-byok/src/client/with-byok.ts
  • packages/ai-byok/src/index.ts
  • packages/ai-byok/src/react.ts
  • packages/ai-byok/tests/byok.test.ts
✅ Files skipped from review due to trivial changes (1)
  • .changeset/byok-package.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/ai-byok/src/index.ts
  • packages/ai-byok/src/react.ts
  • packages/ai-byok/README.md

📝 Walkthrough

Walkthrough

This PR adds @tanstack/ai-byok, a BYOK toolkit that stores provider keys in the browser, sends them via per-provider headers, and adds React, server, example-app, and e2e wiring for missing-key handling and key management.

Changes

Core BYOK package

Layer / File(s) Summary
Shared provider registry and header naming
packages/ai-byok/src/shared/providers.ts
Defines provider metadata, validation config, ProviderId union, and BYOK header naming used across client/server modules.
Client keyring, storage abstraction, and validation
packages/ai-byok/src/client/keyring.ts, .../storage.ts, .../validate.ts
Implements Keyring, byokHeaders, KeyringStorage, memoryStorage, and validateKey.
Passkey-encrypted storage strategy
packages/ai-byok/src/client/passkey.ts
Implements WebAuthn PRF-based AES-GCM encryption, IndexedDB persistence, and passkeyStorage.
Connection wrapper for missing-key detection
packages/ai-byok/src/client/with-byok.ts
Implements byokFetch and withByok to attach headers and detect missing-key 401s.
Server-side key extraction and missing-key/scrubbing utilities
packages/ai-byok/src/server/*
Adds getByokKey, byokMissing, isByokMissingBody, lastFour, maskKey, scrubSecrets, and the server entrypoint.
React BYOK context and useByok hook
packages/ai-byok/src/react/byok-context.tsx, .../use-byok.ts
Implements ByokProvider, status tracking, lock/unlock, load/save/clear, validateKey wiring, and useByok.
ByokKeyManager UI component
packages/ai-byok/src/react/byok-key-manager.tsx
Implements ByokKeyManager with unlock banner, per-provider rows, and status badges.
Package entrypoints, packaging, and docs
packages/ai-byok/src/index.ts, react.ts, server.ts, package.json, tsconfig.json, vite.config.ts, README.md, .changeset/*, knip.json
Adds barrel exports, build config, documentation, release note, workspace config, and package metadata for the new package.
Package unit tests
packages/ai-byok/tests/*
Vitest coverage for headers, withByok/byokFetch, byokFetcher, server helpers, scrubbing, storage, passkey crypto, and React hook behavior.

ts-react-chat example integration

Layer / File(s) Summary
Provider mapping and env status
examples/ts-react-chat/src/lib/byok-config.ts, package.json
Adds BYOK_PROVIDER_MAP, byokIdForProvider, and getEnvKeyStatus.
Relay route BYOK selection and missing-key checks
examples/ts-react-chat/src/routes/api.tanchat.ts
Wraps provider adapters with header-based key selection and returns byokMissing when unavailable.
ByokKeyDialog UI component
examples/ts-react-chat/src/components/ByokKeyDialog.tsx
Adds modal listing providers with key state and entry/clear actions.
Chat page BYOK wiring and storage selection
examples/ts-react-chat/src/routes/index.tsx
Integrates useByok/withByok into SSE connection, adds missing-key handling and UI banners, and introduces ChatRoute selecting passkey vs memory storage.

End-to-end BYOK route and tests

Layer / File(s) Summary
createTextAdapter apiKeyOverride support
testing/e2e/src/lib/providers.ts
Extends createTextAdapter with apiKeyOverride for the OpenAI adapter path.
BYOK e2e API relay route
testing/e2e/src/routes/api.byok-chat.ts
Adds /api/byok-chat handler extracting keys from headers.
BYOK e2e page route
testing/e2e/src/routes/byok.tsx
Adds /byok page with query-based storage selection.
Generated route tree entries for new routes
testing/e2e/src/routeTree.gen.ts
Registers /byok and /api/byok-chat in the generated route tree.
Playwright BYOK tests and docs
testing/e2e/tests/byok.spec.ts, README.md, package.json
Verifies header-only key transmission and missing-key error handling, plus the e2e dependency and test table entry.

Estimated code review effort: 4 (Complex) | ~75 minutes

Possibly related PRs

  • TanStack/ai#512: Adds the fetcher transport path that byokFetcher and the example BYOK wiring integrate with.

Suggested reviewers: AlemTuzlak

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is detailed, but it does not follow the required template and omits the Checklist and Release Impact sections. Rewrite it using the repo template: add the Changes, Checklist, and Release Impact sections, and fill in the required checkbox items.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately names the new @tanstack/ai-byok bring-your-own-key toolkit.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch byok-package

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

🚀 Changeset Version Preview

3 package(s) bumped directly, 0 bumped as dependents.

🟨 Minor bumps

Package Version Reason
@tanstack/ai-byok 0.1.0 → 0.2.0 Changeset
@tanstack/ai-gemini 0.19.1 → 0.20.0 Changeset
@tanstack/ai-openai 0.16.0 → 0.17.0 Changeset

@nx-cloud

nx-cloud Bot commented Jul 7, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit 1c9cdc3

Command Status Duration Result
nx run-many --targets=build --exclude=examples/... ✅ Succeeded 3s View ↗

☁️ Nx Cloud last updated this comment at 2026-07-08 03:36:30 UTC

@nx-cloud

nx-cloud Bot commented Jul 7, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit c9d0226

Command Status Duration Result
nx run-many --targets=build --exclude=examples/... ✅ Succeeded 2m 2s View ↗

☁️ Nx Cloud last updated this comment at 2026-07-07 02:09:29 UTC

@pkg-pr-new

pkg-pr-new Bot commented Jul 7, 2026

Copy link
Copy Markdown

Open in StackBlitz

@tanstack/ai

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai@906

@tanstack/ai-acp

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-acp@906

@tanstack/ai-angular

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-angular@906

@tanstack/ai-anthropic

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-anthropic@906

@tanstack/ai-bedrock

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-bedrock@906

@tanstack/ai-byok

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-byok@906

@tanstack/ai-claude-code

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-claude-code@906

@tanstack/ai-client

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-client@906

@tanstack/ai-code-mode

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-code-mode@906

@tanstack/ai-code-mode-skills

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-code-mode-skills@906

@tanstack/ai-codex

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-codex@906

@tanstack/ai-devtools-core

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-devtools-core@906

@tanstack/ai-elevenlabs

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-elevenlabs@906

@tanstack/ai-event-client

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-event-client@906

@tanstack/ai-fal

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-fal@906

@tanstack/ai-gemini

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-gemini@906

@tanstack/ai-grok

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-grok@906

@tanstack/ai-grok-build

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-grok-build@906

@tanstack/ai-groq

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-groq@906

@tanstack/ai-isolate-cloudflare

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-isolate-cloudflare@906

@tanstack/ai-isolate-node

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-isolate-node@906

@tanstack/ai-isolate-quickjs

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-isolate-quickjs@906

@tanstack/ai-mcp

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-mcp@906

@tanstack/ai-mistral

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-mistral@906

@tanstack/ai-ollama

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-ollama@906

@tanstack/ai-openai

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-openai@906

@tanstack/ai-opencode

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-opencode@906

@tanstack/ai-openrouter

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-openrouter@906

@tanstack/ai-preact

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-preact@906

@tanstack/ai-react

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-react@906

@tanstack/ai-react-ui

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-react-ui@906

@tanstack/ai-sandbox

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox@906

@tanstack/ai-sandbox-cloudflare

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-cloudflare@906

@tanstack/ai-sandbox-daytona

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-daytona@906

@tanstack/ai-sandbox-docker

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-docker@906

@tanstack/ai-sandbox-local-process

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-local-process@906

@tanstack/ai-sandbox-sprites

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-sprites@906

@tanstack/ai-sandbox-vercel

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-vercel@906

@tanstack/ai-solid

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-solid@906

@tanstack/ai-solid-ui

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-solid-ui@906

@tanstack/ai-svelte

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-svelte@906

@tanstack/ai-utils

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-utils@906

@tanstack/ai-vue

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-vue@906

@tanstack/ai-vue-ui

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-vue-ui@906

@tanstack/openai-base

npm i https://pkg.pr.new/TanStack/ai/@tanstack/openai-base@906

@tanstack/preact-ai-devtools

npm i https://pkg.pr.new/TanStack/ai/@tanstack/preact-ai-devtools@906

@tanstack/react-ai-devtools

npm i https://pkg.pr.new/TanStack/ai/@tanstack/react-ai-devtools@906

@tanstack/solid-ai-devtools

npm i https://pkg.pr.new/TanStack/ai/@tanstack/solid-ai-devtools@906

commit: 31a8d72

tombeckenham and others added 7 commits July 7, 2026 17:19
Replace the plaintext localStorage tier with passkey-encrypted
persistence (WebAuthn PRF -> HKDF -> AES-256-GCM ciphertext in
IndexedDB), unwrapped on demand with a biometric/PIN tap. Fully
client-side; protects at-rest, not live in-page XSS (documented).

- passkeyStorage() + isPasskeyStorageSupported() feature detection
- KeyringStorage gains optional `unlockable` + `warning`
- ByokProvider: `locked`/`unlock` so unlockable storage never prompts
  on mount; hydrates on explicit unlock or first save
- ByokKeyManager: unlock banner + storage-specific warning
- memoryStorage() remains the default; no plaintext persistence exists

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tion

- New /byok route: ByokProvider (passkey storage where supported, else
  memory) + ByokKeyManager + a minimal chat that attaches byokHeaders(keys)
- New /api/byok-chat relay: reads the key via getByokKey(request, provider),
  builds the adapter with create{Openai,Anthropic,Gemini}Chat(model, apiKey),
  returns byokMissing() when absent — stateless, no persist/log
- passkeyStorage gains an rpId option; by default the passkey binds to the
  current origin (no hardcoded/central domain)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
New testing/e2e /byok route + /api/byok-chat relay + byok.spec.ts:
- key rides in the x-tanstack-byok-openai header, is absent from the
  request body, streams the aimock response, and the manager shows only
  the last-4
- missing key → byokMissing 401 surfaced as an error, no answer produced

Keyring is hydrated via a preloaded storage (the same load() path passkey
storage uses), so the flow is deterministic without a live WebAuthn
ceremony (passkey crypto + locked/unlock are covered by package unit
tests). Adds an apiKeyOverride to the e2e createTextAdapter and a
byok-masked test hook to ByokKeyManager.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…v-key alerts

Replaces the standalone /byok demo page with front-page integration:
- key icon in the model bar opens a dark-themed dialog for per-provider
  keys (last-4 only), matching the app theme
- getEnvKeyStatus server fn reports which providers have a server env key
  (booleans only, never the value); the key icon shows an amber dot and a
  banner warns when the selected model's provider has no key
- api/tanchat prefers a per-request BYOK header key over env per provider
  (withByok helper); connection attaches byokHeaders(keys)

Removes the old /byok route + api.byok-chat.ts + nav link.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ple uses passkey

Persistent storage can now report presence without decrypting, so the UI
knows keys exist after a refresh:
- KeyringStorage gains optional peek() → { provider: last-4 }
- passkeyStorage stores an unencrypted provider→last-4 sidecar next to the
  ciphertext and reads it via peek() with no unlock ceremony
- new KeyStatus 'locked'; ByokProvider peeks on mount to mark saved keys
  locked (with last-4); unlock() promotes them to 'set' on decrypt

Example front page now uses passkey-encrypted storage when a platform
authenticator is available (else memory). The key dialog shows locked keys
with a lock + last-4 and an "Unlock saved keys" action; the model-bar
warning distinguishes "no key" from "saved but locked → Unlock".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… on byokMissing

The SSE adapter throws a generic HTTP error on non-2xx without the body, so
useChat's error can't identify the missing provider. withByok wires the
provider's fetchClient option to peek at the relay's byokMissing 401 body:

- withByok(getKeys, { onMissingKey }) attaches byokHeaders per request and
  invokes onMissingKey(provider) when the relay returns byokMissing
- byokFetch is the lower-level fetch wrapper; both exported from root + /react
- root now re-exports isByokMissingBody / ByokMissingBody

Example: /api/tanchat returns byokMissing(provider) when it has no server env
key and no BYOK header (instead of a generic 500); the front page uses
withByok, and onMissingKey opens the (now controllable) key dialog focused on
that provider — or, if the key is saved-but-locked, calls unlock() instead.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@tombeckenham tombeckenham marked this pull request as ready for review July 7, 2026 10:41

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🧹 Nitpick comments (8)
packages/ai-byok/tests/react.test.tsx (1)

6-7: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Merge duplicate ../src/index type imports.

Static analysis flags import/no-duplicates for the two separate type-only imports from ../src/index.

🧹 Proposed fix
-import type { Keyring } from '../src/index'
-import type { KeyringStorage } from '../src/index'
+import type { Keyring, KeyringStorage } from '../src/index'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai-byok/tests/react.test.tsx` around lines 6 - 7, The React test
file has duplicate type-only imports from the same module, triggering
import/no-duplicates. Update the import section in react.test.tsx to merge the
Keyring and KeyringStorage type imports into a single import statement from
../src/index, keeping the existing type-only form and preserving the referenced
symbols.

Source: Linters/SAST tools

packages/ai-byok/tests/byok.test.ts (1)

1-16: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Fix import order/sort lint errors.

Static analysis flags: byokHeaderName out of alphabetical order in the named import on Line 2, and the type import on Line 3 should be ordered after the ../src/client/passkey import.

🧹 Proposed fix for import ordering
-import { byokFetch, byokHeaders, byokHeaderName, withByok } from '../src/index'
-import type { Keyring } from '../src/index'
-import {
-  byokMissing,
-  getByokKey,
-  isByokMissingBody,
-  maskKey,
-  scrubSecrets,
-} from '../src/server'
-import { memoryStorage } from '../src/client/storage'
-import {
-  decryptKeyring,
-  deriveAesKey,
-  encryptKeyring,
-} from '../src/client/passkey'
+import { byokFetch, byokHeaderName, byokHeaders, withByok } from '../src/index'
+import {
+  byokMissing,
+  getByokKey,
+  isByokMissingBody,
+  maskKey,
+  scrubSecrets,
+} from '../src/server'
+import { memoryStorage } from '../src/client/storage'
+import {
+  decryptKeyring,
+  deriveAesKey,
+  encryptKeyring,
+} from '../src/client/passkey'
+import type { Keyring } from '../src/index'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai-byok/tests/byok.test.ts` around lines 1 - 16, The test file
import ordering is violating lint rules: the named import from byok helpers has
an out-of-order symbol, and the type-only import is placed before the
client/passkey imports. Reorder the imports in byok.test.ts so the named
specifiers in the ../src/index import are alphabetized and the type import for
Keyring is moved to the correct position after the ../src/client/passkey import,
keeping the existing symbols and groupings intact.

Source: Linters/SAST tools

packages/ai-byok/src/client/passkey.ts (1)

299-322: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Guard ensureKey against concurrent ceremonies.

The doc comment promises "exactly one WebAuthn ceremony," but because the cachedKey/cachedMeta assignment is await-gated, two overlapping calls (e.g. a save racing a load, or rapid double-save) both observe an empty cache, both call idbGet, and both run a full ceremony. In the register branch this can mint two passkeys with only the last record persisted, orphaning the other. Memoize the in-flight promise so concurrent callers share one ceremony.

♻️ Sketch
   let cachedKey: CryptoKey | null = null
   let cachedMeta: {
     credentialId: ArrayBuffer
     salt: Uint8Array<ArrayBuffer>
   } | null = null
+  let pending: Promise<{
+    key: CryptoKey
+    credentialId: ArrayBuffer
+    salt: Uint8Array<ArrayBuffer>
+  }> | null = null
 
   async function ensureKey(): Promise<{
     key: CryptoKey
     credentialId: ArrayBuffer
     salt: Uint8Array<ArrayBuffer>
   }> {
     if (cachedKey && cachedMeta) {
       return { key: cachedKey, ...cachedMeta }
     }
-    const existing = await idbGet(dbName)
-    ...
-    return { key: cachedKey, ...cachedMeta }
+    if (!pending) {
+      pending = (async () => {
+        // ...existing register/unlock logic, sets cachedKey/cachedMeta...
+        return { key: cachedKey!, ...cachedMeta! }
+      })().finally(() => { pending = null })
+    }
+    return pending
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai-byok/src/client/passkey.ts` around lines 299 - 322, The ensureKey
helper currently allows overlapping calls to start separate WebAuthn ceremonies
because cachedKey/cachedMeta are only populated after awaited work completes.
Update ensureKey to memoize and share a single in-flight promise so concurrent
callers (for example load and save, or rapid double-save) all await the same
ceremony; reuse the existing cachedKey/cachedMeta fast path, but ensure the
first unresolved call to ensureKey owns the idbGet/registerPasskey flow and
later calls return that same promise until it settles.
packages/ai-byok/src/client/validate.ts (1)

22-40: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

No timeout on the validation fetch.

fetch has no AbortSignal/timeout, so a stalled network or slow-to-respond provider endpoint leaves the caller's await (and, per byok-context.tsx, the UI's validating state) hanging indefinitely with no way for the user to recover short of a page reload.

⏱️ Proposed fix
 export async function validateKey(
   provider: ProviderId,
   key: string,
 ): Promise<ValidationStatus> {
   const config = providerValidateConfig(provider)
   if (!config) return 'unsupported'

-  const response = await fetch(config.url, {
-    method: 'GET',
-    headers: config.headers(key),
-  })
+  const response = await fetch(config.url, {
+    method: 'GET',
+    headers: config.headers(key),
+    signal: AbortSignal.timeout(10_000),
+  })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai-byok/src/client/validate.ts` around lines 22 - 40, The
validateKey() fetch call currently has no timeout, so a slow or stalled provider
can leave the request and validating state hanging forever. Update validateKey()
in validate.ts to use an AbortSignal with a timeout (for example via
AbortController) around the fetch call, and ensure timeout aborts are handled as
a validation failure instead of hanging. Keep the existing
providerValidateConfig(), response handling, and ValidationStatus behavior
intact.
packages/ai-byok/src/server/byok-missing.ts (1)

39-44: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Type guard doesn't fully validate its asserted shape.

isByokMissingBody only checks error.type, not error.provider/error.message, yet it asserts the full ByokMissingBody interface (Line 4-10). Downstream, with-byok.ts calls onMissingKey(body.error.provider) (Line 23) trusting this is a valid ProviderId — an untrusted/malformed 401 body could pass this guard and hand a bad value to the caller's callback.

♻️ Proposed fix to validate provider/message
 import type { ProviderId } from '../shared/providers'
+import { isProviderId } from '../shared/providers'

 ...

 export function isByokMissingBody(value: unknown): value is ByokMissingBody {
   if (typeof value !== 'object' || value === null) return false
   const { error } = value as { error?: unknown }
   if (typeof error !== 'object' || error === null) return false
-  return (error as { type?: unknown }).type === 'byok_missing'
+  const { type, provider, message } = error as {
+    type?: unknown
+    provider?: unknown
+    message?: unknown
+  }
+  return (
+    type === 'byok_missing' &&
+    typeof provider === 'string' &&
+    isProviderId(provider) &&
+    typeof message === 'string'
+  )
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai-byok/src/server/byok-missing.ts` around lines 39 - 44,
`isByokMissingBody` currently only verifies `error.type`, so it can accept
malformed bodies while claiming to be `ByokMissingBody`. Update the guard in
`byok-missing.ts` to fully validate the asserted shape by checking
`error.provider` and `error.message` alongside `error.type`, ensuring `provider`
matches a valid `ProviderId`-like string and `message` is a string before
returning true. Keep the checks inside `isByokMissingBody` so `with-byok.ts` can
safely call `onMissingKey(body.error.provider)` without trusting unvalidated
data.
packages/ai-byok/src/react/byok-key-manager.tsx (2)

101-107: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

No confirmation before clearing a saved key.

A single click on "Clear" permanently removes the stored key with no undo/confirm step. Worth a lightweight confirm (or a two-step "Clear?" toggle) to prevent accidental deletion, though the user can always re-enter the key.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai-byok/src/react/byok-key-manager.tsx` around lines 101 - 107, The
Clear action in the byok-key-manager component triggers immediate key deletion
with no safeguard, so add a lightweight confirmation step before calling
clearKey(provider). Update the button behavior in ByokKeyManager to require a
second explicit confirmation (or a confirm dialog/toggle) before clearing the
saved key, while keeping the existing clearKey provider flow unchanged once
confirmed.

88-88: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Redundant 'masked' in status check.

hasKey already excludes only the 'empty' state, and every non-'empty' KeyStatus variant carries masked (per byok-context.tsx's KeyStatus union). The extra narrowing is harmless but adds noise.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai-byok/src/react/byok-key-manager.tsx` at line 88, Remove the
redundant masked property check in byok-key-manager.tsx’s render condition: the
hasKey guard already covers all non-empty KeyStatus values, so update the
conditional around the masked UI branch to rely on hasKey alone and keep the
logic aligned with the KeyStatus union defined in byok-context.tsx.
packages/ai-byok/src/react.ts (1)

4-4: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Exporting the raw ByokContext bypasses the useByok safety guard.

Consumers can call useContext(ByokContext) directly and get undefined silently instead of the descriptive error thrown by useByok. Consider keeping ByokContext internal (only exported for testing) and steering public consumers exclusively to useByok.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai-byok/src/react.ts` at line 4, The public re-export in react.ts is
exposing ByokContext directly, which lets consumers bypass the safety checks in
useByok and receive undefined silently. Remove ByokContext from the public
export surface in react.ts, keep it internal or test-only in react/byok-context,
and ensure consumers are directed to use the useByok hook (and ByokProvider) for
all supported access paths.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@examples/ts-react-chat/src/routes/api.tanchat.ts`:
- Around line 22-24: The import block in api.tanchat.ts violates the
import/order lint rule because the value import from BYOK config is placed
before the type-only import. Reorder the imports so the type import from
`@tanstack/ai-byok/server` stays grouped correctly and the value import
BYOK_PROVIDER_MAP/byokIdForProvider from `@/lib/byok-config` comes after it; keep
byokMissing and getByokKey with the other server imports and let the import
grouping in the module follow the linter’s expected order.
- Line 260: The generic type parameter name in byokAdapter violates the
TypeScript naming convention rule; rename M to a valid type-parameter identifier
that matches the required pattern, and update any references to that type
parameter within byokAdapter accordingly. Keep the change scoped to the
byokAdapter declaration and its uses so the lint error is resolved without
altering behavior.

In `@examples/ts-react-chat/src/routes/index.tsx`:
- Around line 37-39: The import order in the route module is violating the
import/order lint rule, so adjust the grouped and sorted imports near the top of
the file. Reorder the type-only and value imports involving ProviderId,
ByokKeyDialog, and byokIdForProvider/getEnvKeyStatus to match the project’s
import grouping conventions, then verify the file passes lint (or use the
auto-fix from the linter).
- Around line 877-882: Remove the unnecessary optional chaining in the
platformAuth calculation inside the index route component:
isPasskeyStorageSupported() already guards globalThis.PublicKeyCredential, so
update the PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()
call to use it directly instead of ?. to satisfy no-unnecessary-condition while
keeping the existing runtime behavior unchanged.

In `@packages/ai-byok/src/client/passkey.ts`:
- Around line 57-64: The support check in isPasskeyStorageSupported() is unsafe
because it dereferences navigator.credentials.create directly, so it can throw
when navigator exists but credentials is undefined. Update the guard in this
function to safely probe navigator.credentials with optional chaining or an
explicit credentials existence check before accessing create, matching the safer
PublicKeyCredential probe so the fallback path can run.

In `@packages/ai-byok/src/react/byok-key-manager.tsx`:
- Around line 121-129: The password input placeholder in byok-key-manager.tsx is
using the raw provider id instead of the user-facing provider label, causing
inconsistent copy with the row header. Update the placeholder logic in the BYOK
key input to use the same label source as the header, namely
BYOK_PROVIDERS[provider].label, while preserving the existing Replace key…
behavior when hasKey is true.
- Around line 93-119: The `ByokKeyManager` action handlers are swallowing async
failures from `setKey` and `clearKey`, so rejected promises never reach the
user. Update the `Validate`, `Clear`, and form submit flows in `ByokKeyManager`
to handle rejections explicitly, similar to how `unlock()` surfaces
`unlockError`, and avoid clearing `draft` until `setKey` succeeds. Use the
existing `validateKey`, `clearKey`, and `setKey` entry points to attach error
handling and show the failure state instead of silently ignoring it.

In `@packages/ai-byok/src/shared/providers.ts`:
- Around line 62-70: The Gemini validation note in providers.ts is inaccurate:
the validate block for the gemini provider uses the x-goog-api-key header, not a
query parameter. Update the inline comment near the gemini validate
configuration to describe the actual header-based authentication used by the
validate URL, so the comment matches the behavior of the gemini provider
definition.

In `@testing/e2e/src/routes/byok.tsx`:
- Around line 4-12: The import order in byok.tsx violates the import/order rule
because the type-only `@tanstack/ai-byok/react` import is placed after the local
`@/components/ChatUI` import. Reorder the imports in the byok module so all
`@tanstack/ai-byok/react` imports (including the Keyring/KeyringStorage type
import) come before the local ChatUI import, keeping the existing grouped
structure intact.

---

Nitpick comments:
In `@packages/ai-byok/src/client/passkey.ts`:
- Around line 299-322: The ensureKey helper currently allows overlapping calls
to start separate WebAuthn ceremonies because cachedKey/cachedMeta are only
populated after awaited work completes. Update ensureKey to memoize and share a
single in-flight promise so concurrent callers (for example load and save, or
rapid double-save) all await the same ceremony; reuse the existing
cachedKey/cachedMeta fast path, but ensure the first unresolved call to
ensureKey owns the idbGet/registerPasskey flow and later calls return that same
promise until it settles.

In `@packages/ai-byok/src/client/validate.ts`:
- Around line 22-40: The validateKey() fetch call currently has no timeout, so a
slow or stalled provider can leave the request and validating state hanging
forever. Update validateKey() in validate.ts to use an AbortSignal with a
timeout (for example via AbortController) around the fetch call, and ensure
timeout aborts are handled as a validation failure instead of hanging. Keep the
existing providerValidateConfig(), response handling, and ValidationStatus
behavior intact.

In `@packages/ai-byok/src/react.ts`:
- Line 4: The public re-export in react.ts is exposing ByokContext directly,
which lets consumers bypass the safety checks in useByok and receive undefined
silently. Remove ByokContext from the public export surface in react.ts, keep it
internal or test-only in react/byok-context, and ensure consumers are directed
to use the useByok hook (and ByokProvider) for all supported access paths.

In `@packages/ai-byok/src/react/byok-key-manager.tsx`:
- Around line 101-107: The Clear action in the byok-key-manager component
triggers immediate key deletion with no safeguard, so add a lightweight
confirmation step before calling clearKey(provider). Update the button behavior
in ByokKeyManager to require a second explicit confirmation (or a confirm
dialog/toggle) before clearing the saved key, while keeping the existing
clearKey provider flow unchanged once confirmed.
- Line 88: Remove the redundant masked property check in byok-key-manager.tsx’s
render condition: the hasKey guard already covers all non-empty KeyStatus
values, so update the conditional around the masked UI branch to rely on hasKey
alone and keep the logic aligned with the KeyStatus union defined in
byok-context.tsx.

In `@packages/ai-byok/src/server/byok-missing.ts`:
- Around line 39-44: `isByokMissingBody` currently only verifies `error.type`,
so it can accept malformed bodies while claiming to be `ByokMissingBody`. Update
the guard in `byok-missing.ts` to fully validate the asserted shape by checking
`error.provider` and `error.message` alongside `error.type`, ensuring `provider`
matches a valid `ProviderId`-like string and `message` is a string before
returning true. Keep the checks inside `isByokMissingBody` so `with-byok.ts` can
safely call `onMissingKey(body.error.provider)` without trusting unvalidated
data.

In `@packages/ai-byok/tests/byok.test.ts`:
- Around line 1-16: The test file import ordering is violating lint rules: the
named import from byok helpers has an out-of-order symbol, and the type-only
import is placed before the client/passkey imports. Reorder the imports in
byok.test.ts so the named specifiers in the ../src/index import are alphabetized
and the type import for Keyring is moved to the correct position after the
../src/client/passkey import, keeping the existing symbols and groupings intact.

In `@packages/ai-byok/tests/react.test.tsx`:
- Around line 6-7: The React test file has duplicate type-only imports from the
same module, triggering import/no-duplicates. Update the import section in
react.test.tsx to merge the Keyring and KeyringStorage type imports into a
single import statement from ../src/index, keeping the existing type-only form
and preserving the referenced symbols.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b9c868f3-0974-4289-8a38-e41c277bf284

📥 Commits

Reviewing files that changed from the base of the PR and between e3de949 and cd6251e.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (35)
  • .changeset/byok-package.md
  • examples/ts-react-chat/package.json
  • examples/ts-react-chat/src/components/ByokKeyDialog.tsx
  • examples/ts-react-chat/src/lib/byok-config.ts
  • examples/ts-react-chat/src/routes/api.tanchat.ts
  • examples/ts-react-chat/src/routes/index.tsx
  • knip.json
  • packages/ai-byok/README.md
  • packages/ai-byok/package.json
  • packages/ai-byok/src/client/keyring.ts
  • packages/ai-byok/src/client/passkey.ts
  • packages/ai-byok/src/client/storage.ts
  • packages/ai-byok/src/client/validate.ts
  • packages/ai-byok/src/client/with-byok.ts
  • packages/ai-byok/src/index.ts
  • packages/ai-byok/src/react.ts
  • packages/ai-byok/src/react/byok-context.tsx
  • packages/ai-byok/src/react/byok-key-manager.tsx
  • packages/ai-byok/src/react/use-byok.ts
  • packages/ai-byok/src/server.ts
  • packages/ai-byok/src/server/byok-missing.ts
  • packages/ai-byok/src/server/get-byok-key.ts
  • packages/ai-byok/src/server/scrub.ts
  • packages/ai-byok/src/shared/providers.ts
  • packages/ai-byok/tests/byok.test.ts
  • packages/ai-byok/tests/react.test.tsx
  • packages/ai-byok/tsconfig.json
  • packages/ai-byok/vite.config.ts
  • testing/e2e/README.md
  • testing/e2e/package.json
  • testing/e2e/src/lib/providers.ts
  • testing/e2e/src/routeTree.gen.ts
  • testing/e2e/src/routes/api.byok-chat.ts
  • testing/e2e/src/routes/byok.tsx
  • testing/e2e/tests/byok.spec.ts

Comment on lines +22 to +24
import { byokMissing, getByokKey } from '@tanstack/ai-byok/server'
import { BYOK_PROVIDER_MAP, byokIdForProvider } from '@/lib/byok-config'
import type { ProviderId } from '@tanstack/ai-byok/server'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Import ordering violates lint (import/order), will fail CI.

ESLint reports the value import of @/lib/byok-config (Line 23) must come after the @tanstack/ai type import. Run lint --fix to reorder.

🧰 Tools
🪛 ESLint

[error] 23-23: @/lib/byok-config import should occur after type import of @tanstack/ai

(import/order)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/ts-react-chat/src/routes/api.tanchat.ts` around lines 22 - 24, The
import block in api.tanchat.ts violates the import/order lint rule because the
value import from BYOK config is placed before the type-only import. Reorder the
imports so the type import from `@tanstack/ai-byok/server` stays grouped correctly
and the value import BYOK_PROVIDER_MAP/byokIdForProvider from `@/lib/byok-config`
comes after it; keep byokMissing and getByokKey with the other server imports
and let the import grouping in the module follow the linter’s expected order.

Source: Linters/SAST tools

// BYOK: prefer a per-request key from the request header, falling back
// to the env-configured adapter. The key is read from the header only,
// never persisted or logged.
function byokAdapter<M extends string>(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Rename type parameter M to satisfy naming-convention rule.

@typescript-eslint/naming-convention requires type params to match /^(T|T[A-Z][A-Za-z]+)$/. This is an [error]-level lint failure.

Proposed rename
-        function byokAdapter<M extends string>(
+        function byokAdapter<TModel extends string>(
           provider: ProviderId,
-          m: M,
-          byok: (model: M, apiKey: string) => AnyTextAdapter,
-          env: (model: M) => AnyTextAdapter,
+          m: TModel,
+          byok: (model: TModel, apiKey: string) => AnyTextAdapter,
+          env: (model: TModel) => AnyTextAdapter,
         ): AnyTextAdapter {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function byokAdapter<M extends string>(
function byokAdapter<TModel extends string>(
provider: ProviderId,
m: TModel,
byok: (model: TModel, apiKey: string) => AnyTextAdapter,
env: (model: TModel) => AnyTextAdapter,
): AnyTextAdapter {
🧰 Tools
🪛 ESLint

[error] 260-260: Type Parameter name M must match the RegExp: /^(T|T[A-Z][A-Za-z]+)$/u

(@typescript-eslint/naming-convention)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/ts-react-chat/src/routes/api.tanchat.ts` at line 260, The generic
type parameter name in byokAdapter violates the TypeScript naming convention
rule; rename M to a valid type-parameter identifier that matches the required
pattern, and update any references to that type parameter within byokAdapter
accordingly. Keep the change scoped to the byokAdapter declaration and its uses
so the lint error is resolved without altering behavior.

Source: Linters/SAST tools

Comment on lines +37 to +39
import type { ProviderId } from '@tanstack/ai-byok/react'
import { ByokKeyDialog } from '@/components/ByokKeyDialog'
import { byokIdForProvider, getEnvKeyStatus } from '@/lib/byok-config'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Import ordering violates lint (import/order), will fail CI.

ESLint flags the @tanstack/ai-byok/react type import (Line 37) and the @/components/ByokKeyDialog / @/lib/byok-config imports (Lines 38-39) as mis-ordered relative to the surrounding type imports. Run lint --fix.

🧰 Tools
🪛 ESLint

[error] 37-37: @tanstack/ai-byok/react type import should occur after import of @tanstack/ai-react-ui

(import/order)


[error] 38-38: @/components/ByokKeyDialog import should occur after type import of @/lib/model-selection

(import/order)


[error] 39-39: @/lib/byok-config import should occur after type import of @/lib/model-selection

(import/order)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/ts-react-chat/src/routes/index.tsx` around lines 37 - 39, The import
order in the route module is violating the import/order lint rule, so adjust the
grouped and sorted imports near the top of the file. Reorder the type-only and
value imports involving ProviderId, ByokKeyDialog, and
byokIdForProvider/getEnvKeyStatus to match the project’s import grouping
conventions, then verify the file passes lint (or use the auto-fix from the
linter).

Source: Linters/SAST tools

Comment on lines +877 to +882
const platformAuth =
isPasskeyStorageSupported() &&
(await globalThis.PublicKeyCredential?.isUserVerifyingPlatformAuthenticatorAvailable().catch(
() => false,
))
if (active) setStorage(platformAuth ? passkeyStorage() : memoryStorage())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm isPasskeyStorageSupported checks for PublicKeyCredential before the optional chain is relied upon.
fd -t f 'passkey' packages/ai-byok/src | xargs -r rg -nP -C3 'isPasskeyStorageSupported|PublicKeyCredential'

Repository: TanStack/ai

Length of output: 1664


Drop the optional chain here. isPasskeyStorageSupported() already guards globalThis.PublicKeyCredential, so ?. is unnecessary and triggers no-unnecessary-condition; remove it rather than weakening the runtime check.

🧰 Tools
🪛 ESLint

[error] 879-879: Unnecessary optional chain on a non-nullish value.

(@typescript-eslint/no-unnecessary-condition)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/ts-react-chat/src/routes/index.tsx` around lines 877 - 882, Remove
the unnecessary optional chaining in the platformAuth calculation inside the
index route component: isPasskeyStorageSupported() already guards
globalThis.PublicKeyCredential, so update the
PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable() call to use
it directly instead of ?. to satisfy no-unnecessary-condition while keeping the
existing runtime behavior unchanged.

Source: Linters/SAST tools

Comment on lines +57 to +64
export function isPasskeyStorageSupported(): boolean {
return (
typeof globalThis !== 'undefined' &&
typeof globalThis.PublicKeyCredential !== 'undefined' &&
typeof globalThis.navigator !== 'undefined' &&
typeof globalThis.navigator.credentials.create === 'function'
)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== packages/ai-byok/src/client/passkey.ts =="
sed -n '1,120p' packages/ai-byok/src/client/passkey.ts

echo
echo "== call sites for isPasskeyStorageSupported =="
rg -n "isPasskeyStorageSupported\(" -S .

echo
echo "== any direct navigator.credentials usage in relevant files =="
rg -n "navigator\.credentials|PublicKeyCredential" packages examples -S

Repository: TanStack/ai

Length of output: 5666


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== examples/ts-react-chat/src/routes/index.tsx around support check =="
sed -n '860,890p' examples/ts-react-chat/src/routes/index.tsx

echo
echo "== README mentions of isPasskeyStorageSupported =="
sed -n '30,120p' packages/ai-byok/README.md

Repository: TanStack/ai

Length of output: 4687


Guard navigator.credentials before accessing create.

isPasskeyStorageSupported() can throw when navigator exists but navigator.credentials is undefined, so the support check itself becomes unsafe and the memory fallback never runs. Use optional chaining here, like the downstream PublicKeyCredential?. probe. packages/ai-byok/src/client/passkey.ts:57-64

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai-byok/src/client/passkey.ts` around lines 57 - 64, The support
check in isPasskeyStorageSupported() is unsafe because it dereferences
navigator.credentials.create directly, so it can throw when navigator exists but
credentials is undefined. Update the guard in this function to safely probe
navigator.credentials with optional chaining or an explicit credentials
existence check before accessing create, matching the safer PublicKeyCredential
probe so the fallback path can run.

Comment on lines +93 to +119
<div style={styles.actions}>
<button
type="button"
style={styles.button}
onClick={() => void validateKey(provider)}
>
Validate
</button>
<button
type="button"
style={styles.button}
onClick={() => void clearKey(provider)}
>
Clear
</button>
</div>
</div>
) : null}

<form
style={styles.inputRow}
onSubmit={(event) => {
event.preventDefault()
if (!draft) return
void setKey(provider, draft)
setDraft('')
}}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Unhandled rejections on setKey/clearKey.

Both calls are fired with void and no .catch, unlike the unlock() handler which surfaces failures via unlockError. If setKey/clearKey reject (e.g. passkey encryption failure, WebAuthn ceremony cancellation, IndexedDB quota errors), the failure is silently swallowed — the input clears (line 118) even though the save may not have persisted, and the user gets no indication anything went wrong.

🐛 Proposed fix to surface save/clear errors
+  const [rowError, setRowError] = useState<string | null>(null)
+
   return (
     <div style={styles.row}>
       ...
             <button
               type="button"
               style={styles.button}
-              onClick={() => void clearKey(provider)}
+              onClick={() =>
+                clearKey(provider).catch((error: unknown) =>
+                  setRowError(error instanceof Error ? error.message : String(error)),
+                )
+              }
             >
               Clear
             </button>
       ...
       <form
         onSubmit={(event) => {
           event.preventDefault()
           if (!draft) return
-          void setKey(provider, draft)
-          setDraft('')
+          setKey(provider, draft)
+            .then(() => setDraft(''))
+            .catch((error: unknown) =>
+              setRowError(error instanceof Error ? error.message : String(error)),
+            )
         }}
       >
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai-byok/src/react/byok-key-manager.tsx` around lines 93 - 119, The
`ByokKeyManager` action handlers are swallowing async failures from `setKey` and
`clearKey`, so rejected promises never reach the user. Update the `Validate`,
`Clear`, and form submit flows in `ByokKeyManager` to handle rejections
explicitly, similar to how `unlock()` surfaces `unlockError`, and avoid clearing
`draft` until `setKey` succeeds. Use the existing `validateKey`, `clearKey`, and
`setKey` entry points to attach error handling and show the failure state
instead of silently ignoring it.

Comment on lines +121 to +129
<input
type="password"
autoComplete="off"
spellCheck={false}
placeholder={hasKey ? 'Replace key…' : `Paste ${provider} key…`}
value={draft}
onChange={(event) => setDraft(event.target.value)}
style={styles.input}
/>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Placeholder shows raw provider id instead of label.

Paste ${provider} key… uses the raw id (e.g. openai) while the row header above shows BYOK_PROVIDERS[provider].label. Minor inconsistency in user-facing copy.

✏️ Proposed fix
-          placeholder={hasKey ? 'Replace key…' : `Paste ${provider} key…`}
+          placeholder={
+            hasKey ? 'Replace key…' : `Paste ${BYOK_PROVIDERS[provider].label} key…`
+          }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<input
type="password"
autoComplete="off"
spellCheck={false}
placeholder={hasKey ? 'Replace key…' : `Paste ${provider} key…`}
value={draft}
onChange={(event) => setDraft(event.target.value)}
style={styles.input}
/>
<input
type="password"
autoComplete="off"
spellCheck={false}
placeholder={
hasKey ? 'Replace key…' : `Paste ${BYOK_PROVIDERS[provider].label} key…`
}
value={draft}
onChange={(event) => setDraft(event.target.value)}
style={styles.input}
/>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai-byok/src/react/byok-key-manager.tsx` around lines 121 - 129, The
password input placeholder in byok-key-manager.tsx is using the raw provider id
instead of the user-facing provider label, causing inconsistent copy with the
row header. Update the placeholder logic in the BYOK key input to use the same
label source as the header, namely BYOK_PROVIDERS[provider].label, while
preserving the existing Replace key… behavior when hasKey is true.

Comment on lines +62 to +70
gemini: {
id: 'gemini',
label: 'Google Gemini',
validate: {
// Gemini authenticates the models list via a query param, not a header.
url: 'https://generativelanguage.googleapis.com/v1beta/models',
headers: (key) => ({ 'x-goog-api-key': key }),
},
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the target file and nearby context.
sed -n '1,140p' packages/ai-byok/src/shared/providers.ts

# Search for Gemini-related validation/request code and comments.
rg -n "x-goog-api-key|generativelanguage.googleapis.com|query param|Gemini authenticates|validate:" packages/ai-byok/src -S

Repository: TanStack/ai

Length of output: 5671


🌐 Web query:

Gemini API x-goog-api-key header docs models list query parameter header documentation

💡 Result:

To authenticate requests to the Gemini API, Google recommends passing your API key in the x-goog-api-key HTTP header rather than including it as a query parameter in the URL [1][2]. This practice improves security by preventing the API key from being exposed in logs or browser history [2]. Documentation for the models list endpoint indicates it is available via a GET request to the following path: GET https://generativelanguage.googleapis.com/v1beta/models To list the available models, you must include the x-goog-api-key header in your request [1][3]. While some legacy examples may still show the API key passed as a?key= query parameter [4][5], the x-goog-api-key header is the preferred, more secure method [2]. Example using cURL: curl https://generativelanguage.googleapis.com/v1beta/models \ -H 'Content-Type: application/json' \ -H "x-goog-api-key: YOUR_API_KEY" The models.list method supports the following optional query parameters to manage pagination [3]: pageSize (integer): The maximum number of models to return per page (default is 50, maximum is 1000) [3]. pageToken (string): A token received from a previous call to retrieve the next page of results [3].

Citations:


Fix the Gemini validation comment The note says “query param,” but this request sends x-goog-api-key in a header; update the comment to match the actual auth mechanism.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai-byok/src/shared/providers.ts` around lines 62 - 70, The Gemini
validation note in providers.ts is inaccurate: the validate block for the gemini
provider uses the x-goog-api-key header, not a query parameter. Update the
inline comment near the gemini validate configuration to describe the actual
header-based authentication used by the validate URL, so the comment matches the
behavior of the gemini provider definition.

Comment on lines +4 to +12
import {
ByokKeyManager,
ByokProvider,
byokHeaders,
memoryStorage,
useByok,
} from '@tanstack/ai-byok/react'
import { ChatUI } from '@/components/ChatUI'
import type { Keyring, KeyringStorage } from '@tanstack/ai-byok/react'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix import order flagged by ESLint.

Static analysis flags the @tanstack/ai-byok/react type import (Line 12) as needing to precede the @/components/ChatUI import (Line 11) per import/order. This will fail the lint gate.

🔧 Proposed fix
 import {
   ByokKeyManager,
   ByokProvider,
   byokHeaders,
   memoryStorage,
   useByok,
 } from '`@tanstack/ai-byok/react`'
+import type { Keyring, KeyringStorage } from '`@tanstack/ai-byok/react`'
 import { ChatUI } from '`@/components/ChatUI`'
-import type { Keyring, KeyringStorage } from '`@tanstack/ai-byok/react`'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import {
ByokKeyManager,
ByokProvider,
byokHeaders,
memoryStorage,
useByok,
} from '@tanstack/ai-byok/react'
import { ChatUI } from '@/components/ChatUI'
import type { Keyring, KeyringStorage } from '@tanstack/ai-byok/react'
import {
ByokKeyManager,
ByokProvider,
byokHeaders,
memoryStorage,
useByok,
} from '`@tanstack/ai-byok/react`'
import type { Keyring, KeyringStorage } from '`@tanstack/ai-byok/react`'
import { ChatUI } from '`@/components/ChatUI`'
🧰 Tools
🪛 ESLint

[error] 12-12: @tanstack/ai-byok/react type import should occur before import of @/components/ChatUI

(import/order)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@testing/e2e/src/routes/byok.tsx` around lines 4 - 12, The import order in
byok.tsx violates the import/order rule because the type-only
`@tanstack/ai-byok/react` import is placed after the local `@/components/ChatUI`
import. Reorder the imports in the byok module so all `@tanstack/ai-byok/react`
imports (including the Keyring/KeyringStorage type import) come before the local
ChatUI import, keeping the existing grouped structure intact.

Source: Linters/SAST tools

tombeckenham and others added 3 commits July 8, 2026 11:08
The BYOK header prefix, IndexedDB name, passkey relying-party name, and
HKDF label all hardcoded "tanstack". None of these need the vendor name:
the header is a private protocol between this package's own client and
server (both resolve it via byokHeaderName), and the storage identifiers
are already overridable per instance. Neutral defaults let the toolkit
read as reusable rather than TanStack-specific.

- header prefix:      x-tanstack-byok-        -> x-byok-
- IndexedDB default:  tanstack-byok           -> byok
- passkey rpName:     "TanStack AI BYOK"      -> "BYOK"
- HKDF info label:    tanstack-byok:keyring:v1 -> byok:keyring:v1

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mirrors withByok (which targets the connection transport) for the fetcher
transport used by useChat/useGeneration. Hands the fetcher body fresh BYOK
headers + a missing-key-aware fetch, covering both a plain fetch call and a
TanStack Start server function (via call-site headers). Keys still travel in
the x-byok-<provider> header, never the body.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant